package io.capsulode.tunnel.blockrepo.proxydef;

import com.fasterxml.jackson.databind.DatabindException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.jsontype.BasicPolymorphicTypeValidator;
import com.fasterxml.jackson.dataformat.yaml.YAMLFactory;
import io.capsulode.tunnel.blockrepo.ProxyDefinition;
import io.capsulode.tunnel.blockrepo.ProxyRepository;
import io.capsulode.tunnel.blockrepo.util.WebUtil;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.util.DigestUtils;

import javax.annotation.PostConstruct;
import javax.annotation.PreDestroy;
import java.io.IOException;
import java.io.OutputStream;
import java.nio.ByteBuffer;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.*;

/**
 * YamlProxyDefinitionRepository
 *
 * @author lipei
 */
public class YamlProxyDefinitionRepository implements ProxyRepository {
    private static final Logger logger = LoggerFactory.getLogger(YamlProxyDefinitionRepository.class);

    private String etag;
    private long lastModified;
    private final Map<String, ProxyDefinition> proxyDefinitions = new HashMap<>();

    private volatile boolean changed = false;

    @Override
    public void put(ProxyDefinition proxy) {
        proxyDefinitions.put(proxy.getName(), proxy);
        changed = true;
        setModified(System.currentTimeMillis());
    }

    @Override
    public void remove(String proxyName) {
        ProxyDefinition old = proxyDefinitions.remove(proxyName);
        if (old != null) {
            changed = true;
            setModified(System.currentTimeMillis());
        }
    }


    private static final Comparator<ProxyDefinition> comparator = Comparator.comparing(ProxyDefinition::getPriority).thenComparing(ProxyDefinition::getName);

    private List<ProxyDefinition> exportProxies() {
        List<ProxyDefinition> proxies = new ArrayList<>(proxyDefinitions.values());
        proxies.sort(comparator);
        return proxies;
    }

    @Override
    public void write(OutputStream out) throws IOException {
        objectMapper.writeValue(out, exportProxies());
    }

    @Override
    public String etag() {
        return etag;
    }

    @Override
    public long lastModified() {
        return lastModified;
    }

    private void setModified(long lastModified) {
        this.lastModified = lastModified;
        ByteBuffer buffer = ByteBuffer.allocate(Long.BYTES);
        buffer.putLong(lastModified);

        etag = DigestUtils.md5DigestAsHex(buffer.array());
    }

    private final ObjectMapper objectMapper = new ObjectMapper(new YAMLFactory())
            .activateDefaultTyping(BasicPolymorphicTypeValidator.builder()
                    .allowIfSubType(ProxyDefinition.class)
                    .build());

    @PostConstruct
    private void startup() throws IOException {
        Path dbPath = getStorage();
        if (!Files.exists(dbPath)) {
            Files.createFile(dbPath);
            String defaultDb = "https://gitee.com/terrason/tunnel/raw/master/block-repo/proxies.db";
            WebUtil.download(defaultDb,dbPath);
            changed = true;
        }

        try {
            ProxyDefinition[] proxyDefinitionArray = objectMapper.readValue(dbPath.toFile(), ProxyDefinition[].class);
            int i = 0;
            for (ProxyDefinition proxyDefinition : proxyDefinitionArray) {
                proxyDefinition.setPriority(i++);
                proxyDefinitions.put(proxyDefinition.getName(), proxyDefinition);
            }
        } catch (DatabindException ex) {
            logger.info("数据文件格式不正确，忽略数据文件！", ex);
        }
        setModified(System.currentTimeMillis());
        logger.info("Yaml Proxy Repository running.");
    }

    @PreDestroy
    private void shutdown() {
        sync();
    }

    @Scheduled(fixedRate = 60000)
    private void sync() {
        if (!changed) {
            return;
        }
        logger.debug("Saving changes!");
        Path dbPath = getStorage();
        try {
            if (!Files.exists(dbPath)) {
                Files.createFile(dbPath);
            }

            objectMapper.writeValue(dbPath.toFile(), exportProxies());
            changed = false;
        } catch (IOException ex) {
            logger.warn("Failed to write to storage: {}", dbPath.toAbsolutePath(), ex);
        }
    }

    private Path getStorage() {
        Path dbPath = Paths.get("proxies.db");
        if (logger.isDebugEnabled()) {
            logger.debug("Use storage: {}", dbPath.toAbsolutePath());
        }
        return dbPath;
    }
}
